Communicating over a Network with Sockets
Even though there are
many network options, MIDP network game programming uses a particular
type of network communication known as a socket.
A socket is a software abstraction for an input or output medium of
communication. More specifically, a socket is a communication channel
enabling you to transfer data through a certain port. The MIDP API
provides socket classes to make programming with sockets much easier.
MIDP sockets are broken down into two types: stream sockets and datagram
sockets.
Stream Sockets
A
stream socket, or connected socket, is a socket over which data can be
transmitted continuously. By continuously, I don’t necessarily mean that
data is being sent all the time, but that the socket itself is active
and ready for communication all the time. Think of a stream socket as a
dedicated network connection, in which a communication medium is always
available for use. The benefit of using a stream socket is that
information can be sent with less worry about when it will arrive at its
destination. Because the communication link is always “live,” data is
generally transmitted immediately after you send it.
Datagram Sockets
The other type of
socket supported by Java is the datagram socket. Unlike stream sockets,
in which the communication is akin to a live network, a datagram socket
is more akin to a dial-up Internet connection, in which the
communication link isn’t continuously active. A datagram socket is a
socket over which data is bundled into packets and sent without
requiring a “live” connection to the destination computer.
Because of the nature of
the communication medium involved, datagram sockets aren’t guaranteed to
transmit information at a particular time, or even in any particular
order. The reason datagram sockets perform this way is that they don’t
require an actual connection to another computer; the address of the
target computer is just bundled with the information being sent. This
bundle is then sent out over the network, leaving the sender to hope for
the best. On the receiving end, the bundles of information can be
received in any order and at any time. For this reason, datagrams also
include a sequence number that specifies to which piece of the puzzle
each bundle corresponds. The receiver waits to receive the entire
sequence, and then puts them back together to form a complete
information transfer.
You might be thinking
that datagram sockets are less than ideal for network game programming,
and in some cases this is true. However, not all games require the
“live” connection afforded by stream sockets. And in the specific case
of mobile games, datagram sockets are often a more realistic networking
option because of the limited bandwidth of mobile phone networks.
Network Programming and J2ME
Network programming in MIDlets is carried out using a portion of the MIDP API known as the Generic Connection Framework,
or GCF. The purpose of the GCF is to provide a level of abstraction for
networking services, which helps in enabling different mobile devices
to support only network protocols specific to their needs.
Although it is
structured somewhat differently, the GCF is implemented as a functional
subset of the J2SE API. The GCF describes one fundamental class named Connector
that is used to establish all MIDlet network connections. Specific
types of network connections are modeled by interfaces that are obtained
through the Connector class. The Connector class and the connection interfaces are located in the javax.microedition.io package. Descriptions of a few of these interfaces follow:
ContentConnection— A stream connection that provides access to web data
DatagramConnection— A datagram connection suitable for handling packet-oriented communication
StreamConnection— A two-way connection to a communications device
From the perspective of mobile game programming, you will typically use either the DatagramConnection or StreamConnection interfaces as the network connection types for games. You always use the Connector class to establish network connections, regardless of the connection type. All the methods in the Connector class are static, with the most important one being the open() method. The most commonly used version of the open() method follows:
static Connection open(String name) throws IOException
The parameter to
this method is the connection string, which determines the type of
connection being made. The connection string describes the connection by
adhering to the following general form:
Scheme:Target[;Parameters]
The Scheme parameter is the name of the network protocol, such as http, ftp, or datagram. The Target
parameter is typically the name of the network address for the
connection, but can vary according to the specific protocol. The last
parameter, Parameters,
is a list of parameters associated with the connection. Some examples
of different types of connection strings for various network connections
are
HTTP— "http://www.stalefishlabs.com/"
Socket— "socket://www.stalefishlabs:1800"
Datagram— "datagram://:9000"
File— "file:/Stats.txt"
Keep in mind that
although these examples are accurate in terms of describing possible
connection strings, the only one of them that has guaranteed support in a
given MIDP implementation is the first one. The MIDP specification
requires an implementation only to support HTTP connections. If you
happen to know that a given MIDP implementation supports a particular
type of connection, then you can certainly take advantage of it.
Otherwise, you will need to stick with HTTP connections, which
admittedly aren’t too useful in networked mobile games.
The open() method returns an object of type Connection,
which is the base interface for all the other connection interfaces. To
use a certain kind of connection interface, you cast the Connection interface returned by open() to the appropriate type. The following line of code illustrates how to use the DatagramConnection interface to open a datagram connection:
DatagramConnection dc = (DatagramConnection)Connector.open("datagram://:5555");
Construction Cue
The number 5555
in this example code is the network port used by the datagram
connection. This port number can be any value over 1024, but it’s very
important for the client and server code in a networked MIDlet to
communicate through the same port number. |
The next few sections dig a little deeper into datagram connections, and how to send and receive data through them.
Creating Datagram Packets
Using datagrams to
communicate over a mobile phone network involves packaging game data
into discrete bundles of information called packets. When mobile games
shuttle information back and forth through a datagram connection, they
are actually sending and receiving packets. Datagram packets are
designed to store an array of bytes, so any data that you package into a
packet must be converted into a byte array. In fact, when you create a Datagram object you must specify how many bytes of data it is capable of holding. Following is an example of creating a Datagram object large enough to hold 64 bytes of data:
Datagram dg = dc.newDatagram(64);
What you may find interesting in this code is that you create a Datagram object by calling the newDatagram() method on a datagram connection. The parameter to the newDatagram()
method is the size of the datagram, in bytes. This approach to creating
a datagram packet is ideal for receiving game data sent over a network
connection that gets stored in the datagram.
Another approach to
creating a datagram packet is to create and fill the datagram with data
in one step. This approach is better suited to sending game data, in
which case you already have data that you want to place in the datagram.
Many games use string messages to communicate, in which case each
string must be first converted into a byte array before it is stored in a
datagram, as the following code shows:
String message = "GameOver";
byte[] bytes = message.getBytes();
In this code, the string "GameOver" is converted into a byte array that is stored in the variable bytes. A different version of the newDatagram() method is then used to create a datagram packet containing the "GameOver" message:
Datagram dg = dc.newDatagram(bytes, bytes.length);
In this code, the byte array of game data is passed as the first parameter to the newDatagram()
method, whereas the length of the data is passed as the second
parameter. In some situations (a packet being sent from a server to a
client), you need to use yet another version of the newDatagram() method, like this:
Datagram dg = dc.newDatagram(bytes, bytes.length, address);
This method includes a
third parameter that contains the address of the target to receive the
datagram packet. An address is required only when a server is
communicating to a client, in which case the address can be obtained
from a client datagram with a call to the getAddress() method on a datagram received from the client.
Sending Datagram Packets
The DatagramConnection interface provides a single method for sending datagram packets. I’m referring to the send() method, which is extremely easy to use. In fact, a single line of code is all that is required to send a datagram packet:
To help put this line of code in context, check out the following code, which shows how a Datagram object is created and sent over a datagram connection:
// Convert the string message to bytes
byte[] bytes = message.getBytes();
// Send the message
Datagram dg = null;
dg = dc.newDatagram(bytes, bytes.length);
dc.send(dg);
You’ve already seen all
this code in pieces, but here you see it assembled together. This is
really all that is required to package up game data into a datagram
packet and send it over a wireless network connection.
Receiving Datagram Packets
Receiving a datagram packet is somewhat similar to sending a packet in that a single method of the DatagramConnection interface is used. The method is called receive(), and it accepts a single Datagram object as its only parameter, just like send(). Following is an example of calling the receive() method to receive a datagram packet:
Of course, the datagram
packet in this case needs to have already been created and sized large
enough to hold the incoming data. Following is a more complete example
of creating a datagram packet and then using it to receive incoming game
data:
// Try to receive a datagram packet
Datagram dg = dc.newDatagram(64);
dc.receive(dg);
// Make sure the datagram actually contains data
if (dg.getLength() > 0) {
String data = new String(dg.getData(), 0, dg.getLength());
}
It’s important to notice in this code that the resulting datagram length is checked via a call to getLength().
This check is important because it lets you know whether any data was
actually received in the datagram packet. If there is indeed data, it is
converted back to a string and stored in the data variable. It is then up to game-specific code to respond to the received data.